spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { IncidentSeriesChart } from '@/components/incidents/IncidentSeriesChart';4import { TYPE_LABEL } from '@/components/incidents/IncidentRow';5import { Bar, ConfBar, Section, Stat, StatusDot } from '@/components/ui/primitives';6import { apiGet, apiTry } from '@/lib/api';7import { fmt, fmtDuration, fmtInt, fmtRatio } from '@/lib/format';8import { pressureColor } from '@/lib/pressure';9import { Time } from '@/lib/time';10import type { IncidentDetail } from '@/lib/types';1112export const dynamic = 'force-dynamic';1314type Params = Promise<{ slug: string }>;1516export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {17 const { slug } = await params;18 const i = await apiTry<IncidentDetail>(`/api/v1/incident/${encodeURIComponent(slug)}`);19 if (!i) return { title: 'Incident' };20 return { title: `${i.title} — ${i.status}, peak ${fmt(i.peak_pressure)}`, description: i.summary, alternates: { canonical: `/event/${i.slug}` }, openGraph: { type: 'article', publishedTime: i.started_at, modifiedTime: i.updated_at } };21}2223function scopeHref(i: IncidentDetail): string | null {24 if (i.scope_type === 'region' && i.scope_id) return `/internet/${i.scope_id}`;25 if (i.scope_type === 'country' && i.scope_id) return `/country/${i.scope_id.toLowerCase()}`;26 if (i.scope_type === 'asn' && i.scope_id) return `/asn/${i.scope_id}`;27 if (i.scope_type === 'service' && i.scope_id) return `/service/${i.scope_id}`;28 return null;29}3031export default async function EventPage({ params }: { params: Params }) {32 const { slug } = await params;33 const i = await apiGet<IncidentDetail>(`/api/v1/incident/${encodeURIComponent(slug)}`);34 const href = scopeHref(i);35 return (36 <article className="pb-8">37 <header className="grid gap-6 pt-6 pb-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end">38 <div className="min-w-0">39 <p className="label flex flex-wrap items-center gap-3">40 <span>{TYPE_LABEL[i.type] ?? i.type}</span>41 <StatusDot status={i.status} />42 <span className="num normal-case tracking-normal text-ink-3">{i.event_id}</span>43 </p>44 <h1 className="mt-1 text-[26px] font-medium leading-tight tracking-tight sm:text-[32px]">{i.title}</h1>45 <p className="mt-2 max-w-[760px] text-[14px] text-ink-2">{i.summary}</p>46 <p className="num mt-3 flex flex-wrap gap-x-5 gap-y-1 text-[12px] text-ink-2">47 <span>48 scope{' '}49 {href ? (50 <Link href={href} className="text-ink hover:text-accent">51 {i.scope_label}52 </Link>53 ) : (54 <span className="text-ink">{i.scope_label}</span>55 )}56 </span>57 <span>58 started <Time ts={i.started_at} className="text-ink" />59 </span>60 <span>61 {i.ended_at ? (62 <>63 ended <Time ts={i.ended_at} className="text-ink" />64 </>65 ) : (66 <>67 updated <Time ts={i.updated_at} className="text-ink" />68 </>69 )}70 </span>71 <span>72 duration <span className="text-ink">{fmtDuration(i.duration_s)}</span>73 </span>74 </p>75 </div>76 <div className="flex flex-wrap items-end gap-6">77 <div>78 <div className="label">current</div>79 <div className="num text-[56px] leading-none tracking-[-0.03em]" style={{ color: pressureColor(i.current_pressure) }}>80 {fmt(i.current_pressure)}81 </div>82 </div>83 <div>84 <div className="label">peak</div>85 <div className="num text-[28px] leading-none" style={{ color: pressureColor(i.peak_pressure) }}>86 {fmt(i.peak_pressure)}87 </div>88 </div>89 <div className="pb-1">90 <div className="label mb-1">confidence</div>91 <ConfBar value={i.confidence} />92 </div>93 </div>94 </header>9596 <Section label="Series" right={<span>incident scope vs global pressure · from 30 min before detection</span>}>97 <IncidentSeriesChart series={i.series} timeline={i.timeline} />98 </Section>99100 <div className="grid grid-cols-[minmax(0,1fr)] gap-x-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">101 <Section label="Timeline" className="min-w-0">102 <ol className="relative ml-2 border-l border-line">103 {i.timeline.map((t, idx) => (104 <li key={idx} className="relative mb-4 pl-5 last:mb-0">105 <span className="absolute -left-[5px] top-1.5 size-2 rounded-full border border-bg" style={{ background: pressureColor(t.pressure) }} aria-hidden="true" />106 <div className="flex flex-wrap items-baseline gap-x-3">107 <StatusDot status={t.status} />108 <Time ts={t.ts} className="num text-[11.5px] text-ink-2" />109 <span className="num text-[12px]" style={{ color: pressureColor(t.pressure) }}>110 {fmt(t.pressure)}111 </span>112 </div>113 <p className="mt-0.5 text-[12.5px] text-ink-2">{t.note}</p>114 </li>115 ))}116 </ol>117 </Section>118 <Section label="Hypotheses" right={<span>never overstated · confidence-weighted</span>} className="min-w-0">119 {i.hypotheses.length ? (120 <ul className="space-y-4">121 {i.hypotheses.map((h, idx) => (122 <li key={idx}>123 <div className="flex items-baseline justify-between gap-4">124 <p className="text-[14px] text-ink">{h.text}</p>125 <span className="num shrink-0 text-[12px] text-ink-2">{Math.round(h.confidence * 100)} %</span>126 </div>127 <Bar value={h.confidence * 100} color="var(--accent)" className="mt-1.5" />128 <ul className="mt-2 space-y-0.5 text-[12.5px] text-ink-2">129 {h.evidence.map((e, k) => (130 <li key={k}>· {e}</li>131 ))}132 </ul>133 </li>134 ))}135 </ul>136 ) : (137 <p className="text-[12.5px] text-ink-3">No causal hypothesis reached the minimum confidence.</p>138 )}139 </Section>140 </div>141142 <Section label="Evidence" right={<span>signal · scope · current vs baseline · robust z</span>}>143 <div className="scroll-x -mx-3 px-3">144 <table className="tbl">145 <thead>146 <tr>147 <th>Signal</th>148 <th className="hidden sm:table-cell">Scope</th>149 <th className="r">Current</th>150 <th className="r">Baseline</th>151 <th className="r">z</th>152 <th className="r hidden sm:table-cell">Samples</th>153 <th className="r hidden md:table-cell">At</th>154 </tr>155 </thead>156 <tbody>157 {i.evidence.map((e, idx) => (158 <tr key={idx}>159 <td>160 <span className="text-ink">{e.label}</span> <span className="num text-[10.5px] text-ink-3">{e.signal_id}</span>161 </td>162 <td className="hidden text-ink-2 sm:table-cell">{e.scope_id ? `${e.scope_type}:${e.scope_id}` : e.scope_type}</td>163 <td className="num r">{fmt(e.current, Math.abs(e.current) < 1 ? 3 : 1)}</td>164 <td className="num r text-ink-2">{fmt(e.baseline, Math.abs(e.baseline) < 1 ? 3 : 1)}</td>165 <td className="num r" style={{ color: Math.abs(e.robust_z) >= 3 ? 'var(--p-high)' : 'var(--ink)' }}>166 {fmt(e.robust_z)}167 </td>168 <td className="num r hidden text-ink-2 sm:table-cell">{fmtInt(e.samples)}</td>169 <td className="num r hidden text-ink-2 md:table-cell">170 <Time ts={e.ts} style="time" />171 </td>172 </tr>173 ))}174 </tbody>175 </table>176 </div>177 {i.bgp && (178 <div className="mt-4 grid grid-cols-3 gap-4 sm:max-w-[480px]">179 <Stat label="BGP withdrawals" value={fmtRatio(i.bgp.withdrawals_ratio)} sub="vs baseline" />180 <Stat label="BGP announcements" value={fmtRatio(i.bgp.announcements_ratio)} sub="vs baseline" />181 <Stat label="origin changes" value={fmtInt(i.bgp.origin_changes)} />182 </div>183 )}184 </Section>185186 <div className="grid grid-cols-[minmax(0,1fr)] gap-x-10 lg:grid-cols-2">187 <Section label={`Affected probes · ${fmtInt(i.affected_probes)}`} className="min-w-0">188 <ul className="divide-y divide-line text-[12.5px]">189 {i.probes.map((p) => (190 <li key={p.probe_id} className="flex flex-wrap gap-x-3 py-1.5">191 <span className="num w-24 text-ink">{p.probe_id}</span>192 <Link href={`/internet/${p.region}`} className="w-24 text-ink-2 hover:text-accent">193 {p.region}194 </Link>195 <span className="flex-1 text-ink-2">{p.observation}</span>196 </li>197 ))}198 </ul>199 </Section>200 <Section label={`Affected targets · ${fmtInt(i.affected_targets)}`} className="min-w-0">201 <ul className="divide-y divide-line text-[12.5px]">202 {i.targets.map((t) => (203 <li key={t.target_id} className="flex flex-wrap gap-x-3 py-1.5">204 <Link href={`/service/${t.service_id}`} className="w-40 truncate text-ink hover:text-accent">205 {t.name}206 </Link>207 <span className="flex-1 text-ink-2">{t.observation}</span>208 </li>209 ))}210 </ul>211 <div className="mt-3 flex flex-wrap gap-1.5">212 {i.affected_asns.map((a) => (213 <Link key={a} href={`/asn/${a}`} className="num rounded-[3px] border border-line px-1.5 py-0.5 text-[11px] text-ink hover:border-line-2">214 AS{a}215 </Link>216 ))}217 {i.affected_services.map((s) => (218 <Link key={s} href={`/service/${s}`} className="rounded-[3px] border border-line px-1.5 py-0.5 text-[11px] text-ink hover:border-line-2">219 {s}220 </Link>221 ))}222 </div>223 </Section>224 </div>225226 <Section label="Annotations">227 {i.annotations.length ? (228 <ul className="divide-y divide-line text-[13px]">229 {i.annotations.map((a, idx) => (230 <li key={idx} className="py-2">231 <p className="text-ink">{a.text}</p>232 <p className="num mt-0.5 text-[11px] text-ink-3">233 {a.author} · <Time ts={a.ts} />234 </p>235 </li>236 ))}237 </ul>238 ) : (239 <p className="text-[12.5px] text-ink-3">No human annotation yet.</p>240 )}241 </Section>242 </article>243 );244}245